Add aspire destroy command for tearing down deployed environments#16097
Add aspire destroy command for tearing down deployed environments#16097
Conversation
Implements #13013 - adds a top-level 'aspire destroy' command that tears down previously deployed Aspire environments. The command follows the same pipeline architecture as 'aspire deploy' and 'aspire publish'. Changes: - Add WellKnownPipelineSteps.Destroy and DestroyPrereq aggregation steps - Add DestroyCommand CLI command with --yes flag to skip confirmation - Add destroy-prereq step with interactive confirmation prompt - Wire Docker Compose's existing docker-compose-down step to destroy - Wire Kubernetes Helm's existing helm-uninstall step to destroy - Add Azure resource group deletion via ARM SDK for ACA/App Service - Add IResourceGroupResource.DeleteAsync to provisioning abstractions - Add PipelineOptions.Yes for forwarding --yes flag to AppHost - Update pipeline step count test and accept diagnostics snapshots Validated with pipeline tests (65), Docker Compose tests (85), Kubernetes tests (88), and Azure deployer tests (28) all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the prereq generic — each environment step already surfaces target-specific details (resource group, Helm release, compose project). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Query the resource group via ARM to list all resources before deleting,
so users can see exactly what will be destroyed. The discovery phase
logs each resource type and name, then reports the total count.
Pipeline output:
Discovering resources in myapp-rg
ContainerApps/containerApps: api
KeyVault/vaults: kv-myapp
ContainerRegistry/registries: acrmyapp
Found 3 resource(s) in myapp-rg
Deleting resource group myapp-rg (3 resource(s))
If enumeration fails (e.g. permissions), deletion still proceeds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DestroyCommandTests: help, invalid project, --step destroy argument, --yes flag forwarding, --output-path inclusion (5 tests) - K8s: HelmUninstallStep_RequiredByDestroy verifies helm-uninstall depends on destroy-prereq - Register DestroyCommand in test DI (CliTestHelper.cs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each environment step now owns its own confirmation with full context:
- Azure: discovers resources in RG, then asks to confirm deletion
- Docker Compose: asks to confirm compose down
- Kubernetes: asks to confirm helm uninstall with release name + namespace
Pipeline layering: destroy → destroy-{env} (prompt) → action step (no prompt)
This means 'aspire do docker-compose-down' skips the prompt (explicit action),
while 'aspire destroy' chains through the confirmation layer.
The generic destroy-prereq is now a plain no-op placeholder step.
--yes skips all confirmation prompts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace manual cleanup commands with 'aspire destroy --yes': - Azure (14 files): add aspire destroy step before exit, keep CleanupResourceGroupAsync as safety net in finally block - Docker (2 tests): replace 'docker compose down' with aspire destroy - Podman (1 test): replace 'podman compose down' with aspire destroy - Kubernetes (1 test): replace 'helm uninstall' with aspire destroy, keep KinD cluster deletion separate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fail fast when --yes is not set and interactivity is unavailable, instead of silently proceeding with destruction - Consolidate destroy steps: each environment's destroy step does confirm + action in one step, keeping standalone action steps (docker-compose-down, helm-uninstall) clean for aspire do usage - destroy-prereq is now a plain no-op placeholder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The destroy aggregation step now deletes the deployment state file after all environment destroy steps succeed, acting as an implicit cache clear. This ensures the next deploy starts fresh. Removed per-section Azure state cleanup since the whole file is now deleted at the end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move non-interactive/--yes check before ARM calls in Azure destroy so it fails fast without doing expensive Azure work - Document that state file deletion is intentional (includes saved parameters — expected for full environment teardown) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tell the user to pass the same --output-path they used during deploy, instead of just saying the file doesn't exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include the actual error output (e.g. 'Cannot connect to Docker daemon') instead of generic 'ensure runtime is installed' guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Save minimal deployment state during deploy so destroy can verify
what was actually deployed:
Docker Compose: saves OutputPath, ProjectName, ComposeFilePath
to DockerCompose:{name} state section during compose-up
Helm: saves ReleaseName, Namespace
to Helm:{name} state section during helm-deploy
Destroy steps now check for deployment state first and report
'Nothing to destroy' instead of failing with confusing errors
when no deployment exists.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Merge Azure destroy step into existing PipelineStepAnnotation 2. Flatten await using blocks to reduce nesting 3. Remove global::Azure prefix (using Azure; works fine) 4. Add IDeploymentStateManager.ClearAllStateAsync for centralized cleanup 5. Per-environment destroy steps now clean up their own state sections 6. Extract shared AspireDestroyAsync helper for E2E test cleanup, removing duplication across 17 test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Full destroy is a full reset — clears parameters, Azure config, and per-environment state. Per-section cleanup in environment steps handles partial/scoped operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each environment now adds summary entries showing what was destroyed: - Azure: resource group name + subscription - Docker Compose: environment name - Helm: release name + namespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate all state file mutations through IDeploymentStateManager so in-memory state is also reset correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three new tests covering the destroy pipeline for Azure: - WithAzureState: verifies RG discovery and deletion runs - WithNoAzureState: verifies 'Nothing to destroy' message - NonInteractiveWithoutYes: verifies fail-fast with --yes guidance Added InMemoryDeploymentStateManager for stateful test scenarios. Added deploymentStateManager parameter to ConfigureTestServices. Updated all IDeploymentStateManager mocks with ClearAllStateAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reliability fixes from code review: - Docker destroy now uses saved ComposeFilePath/ProjectName from deployment state instead of recomputing from current model - Helm destroy uses saved ReleaseName/Namespace for both the confirmation prompt and the actual uninstall call - Docker destroy only clears state after successful compose down, preserves state when compose file is missing Test improvements: - Extract InMemoryDeploymentStateManager to shared test code - Add FakeContainerRuntime.WasComposeDownCalled tracking - Add 2 Docker Compose destroy pipeline tests: - WithState: verifies compose down is called via FakeContainerRuntime - WithNoState: verifies 'Nothing to destroy' without calling compose down Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract IHelmRunner interface from HelmDeploymentEngine to enable testability of Helm operations without requiring a real helm binary. - IHelmRunner: abstraction for running helm CLI commands - DefaultHelmRunner: production implementation using ProcessUtil - FakeHelmRunner: test double that tracks calls and returns exit code 0 - Refactor HelmDeployAsync and HelmUninstallAsync to use IHelmRunner - Register DefaultHelmRunner in DI via AddKubernetesInfrastructureCore New tests: - DestroyHelm_WithState: verifies helm uninstall is called with saved release name and namespace from deployment state - DestroyHelm_WithNoState: verifies 'Nothing to destroy' without calling helm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Make TestResourceGroupResource observable with WasDeleteCalled and WasGetResourcesCalled tracking, threaded through ARM client/subscription - Azure destroy test now asserts ARM DeleteAsync and GetResourcesAsync were actually called, not just that the step was created - Add deploy→destroy roundtrip test for Docker Compose that verifies state persisted during deploy is correctly consumed by destroy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clearer intent — 'Yes' was ambiguous, 'SkipConfirmation' describes exactly what the option does. The CLI flag remains --yes/-y. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16097Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16097" |
Verifies that when helm uninstall exits non-zero, deployment state is preserved so the user can retry aspire destroy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
src/Aspire.Hosting.Kubernetes/Deployment/HelmDeploymentEngine.cs
Outdated
Show resolved
Hide resolved
- Lowercase non-proper-noun words in prompt titles (JamesNK) - Revert --force back to --yes pending naming discussion - Fix zero-width character introduced by sed in test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JamesNK
left a comment
There was a problem hiding this comment.
2 issues found: 1 bug (double task completion in HelmUninstallAsync), 1 formatting issue.
docker compose down validates the compose file before executing, which fails when build contexts referenced in the file no longer exist on disk. This happens in normal deploy→destroy flows when containers were built from temporary contexts. Fix: destroy now uses project-name-only mode (no -f flag) for compose down. The project name from saved deployment state is sufficient — compose looks up running containers by project label. Also make ComposeFilePath optional in ComposeOperationContext so callers can opt into project-name-only mode. Test updated to verify ComposeFilePath is null in destroy context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change from WaitUntil.Started to WaitUntil.Completed so destroy actually waits for the RG to be deleted before reporting success. This avoids the confusing 'deletion initiated' message and prevents the stale-state problem where immediate redeploy fails. Also closes #16100 since we now wait for completion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove redundant FailAsync before throw in HelmUninstallAsync to avoid completing the task twice (JamesNK) - Fix 8-space indent in ProvisioningTestHelpers (JamesNK) - Revert Azure delete to WaitUntil.Started (faster for E2E tests) with honest messaging: 'deletion in progress, monitor in Azure portal' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show 'Deletion in progress. Monitor in the Azure portal.' in the pipeline summary so the async nature of RG deletion is visible even after the pipeline output scrolls away. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the monitoring link unmissable by showing the full URL rather than hiding it behind 'Azure portal' link text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e7f6a56 to
72fe4d7
Compare
| PrimaryButtonText = "Destroy", | ||
| SecondaryButtonText = "Cancel" |
There was a problem hiding this comment.
Do these need to be localized?
There was a problem hiding this comment.
I guess probably not - since all the other strings we present to the user here aren't.
| context.CancellationToken).ConfigureAwait(false); | ||
|
|
||
| var resources = new List<(string Name, string ResourceType)>(); | ||
| await using var _ = discoveryTask.ConfigureAwait(false); |
There was a problem hiding this comment.
Are these supposed to go in a scope so they get disposed before the end of the method?
| await resourceGroup.DeleteAsync(WaitUntil.Started, context.CancellationToken).ConfigureAwait(false); | ||
|
|
||
| var portalUrl = AzurePortalUrls.GetResourceGroupUrl(subscriptionId, resourceGroupName, subscription.TenantId); | ||
| context.Summary.Add("🗑️ Resource Group", new MarkdownString($"[{resourceGroupName}]({portalUrl})")); |
There was a problem hiding this comment.
What's the point of logging the portal URL to something that no longer exists? So the user can verify it doens't exist?
There was a problem hiding this comment.
Because this is fully async. You run destroy and it exits immediately but the operation is still in progess.
|
|
||
| var portalUrl = AzurePortalUrls.GetResourceGroupUrl(subscriptionId, resourceGroupName, subscription.TenantId); | ||
| context.Summary.Add("🗑️ Resource Group", new MarkdownString($"[{resourceGroupName}]({portalUrl})")); | ||
| context.Summary.Add("🔑 Subscription", subscriptionId); |
There was a problem hiding this comment.
Maybe instead log a URL to the subscription?
| private static async Task ConfirmDestroyAsync(PipelineStepContext context, string message) | ||
| { | ||
| var options = context.Services.GetRequiredService<IOptions<PipelineOptions>>(); | ||
|
|
||
| if (!options.Value.SkipConfirmation) | ||
| { | ||
| var interactionService = context.Services.GetRequiredService<IInteractionService>(); | ||
|
|
||
| if (!interactionService.IsAvailable) | ||
| { | ||
| throw new InvalidOperationException( | ||
| "Cannot perform destructive operation without confirmation. Use --yes to skip the confirmation prompt in non-interactive mode."); | ||
| } | ||
|
|
||
| var result = await interactionService.PromptNotificationAsync( | ||
| "Destroy environment", | ||
| message, | ||
| new NotificationInteractionOptions | ||
| { | ||
| Intent = MessageIntent.Confirmation, | ||
| ShowSecondaryButton = true, | ||
| ShowDismiss = false, | ||
| PrimaryButtonText = "Destroy", | ||
| SecondaryButtonText = "Cancel" |
There was a problem hiding this comment.
(nit) why is the message get passed in, but all the other strings aren't? Seems like we could just hard code the message here as well.
- Scope discoveryTask await using block so it disposes before confirmation prompt - Inline Docker compose destroy confirmation message into ConfirmDestroyAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
davidfowl
left a comment
There was a problem hiding this comment.
Addressed feedback from @eerhardt in 84ce8cf:
- Scoped
discoveryTaskawait usingblock so it disposes before the confirmation prompt - Inlined the Docker compose destroy confirmation message into
ConfirmDestroyAsync(takesenvironmentNamenow instead of a message string) - Localization: agreed, not needed since other strings in this area aren't localized
- Portal URL: keeping as-is since the RG URL still shows deletion status while the async operation is in progress
|
🎬 CLI E2E Test Recordings — 68 recordings uploaded (commit View recordings
📹 Recordings uploaded automatically from CI run #24352396979 |
…6097) * Add aspire destroy command for tearing down deployed environments Implements #13013 - adds a top-level 'aspire destroy' command that tears down previously deployed Aspire environments. The command follows the same pipeline architecture as 'aspire deploy' and 'aspire publish'. Changes: - Add WellKnownPipelineSteps.Destroy and DestroyPrereq aggregation steps - Add DestroyCommand CLI command with --yes flag to skip confirmation - Add destroy-prereq step with interactive confirmation prompt - Wire Docker Compose's existing docker-compose-down step to destroy - Wire Kubernetes Helm's existing helm-uninstall step to destroy - Add Azure resource group deletion via ARM SDK for ACA/App Service - Add IResourceGroupResource.DeleteAsync to provisioning abstractions - Add PipelineOptions.Yes for forwarding --yes flag to AppHost - Update pipeline step count test and accept diagnostics snapshots Validated with pipeline tests (65), Docker Compose tests (85), Kubernetes tests (88), and Azure deployer tests (28) all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify destroy-prereq confirmation message Keep the prereq generic — each environment step already surfaces target-specific details (resource group, Helm release, compose project). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enumerate Azure resources before destroying resource group Query the resource group via ARM to list all resources before deleting, so users can see exactly what will be destroyed. The discovery phase logs each resource type and name, then reports the total count. Pipeline output: Discovering resources in myapp-rg ContainerApps/containerApps: api KeyVault/vaults: kv-myapp ContainerRegistry/registries: acrmyapp Found 3 resource(s) in myapp-rg Deleting resource group myapp-rg (3 resource(s)) If enumeration fails (e.g. permissions), deletion still proceeds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add DestroyCommand unit tests and pipeline wiring tests - DestroyCommandTests: help, invalid project, --step destroy argument, --yes flag forwarding, --output-path inclusion (5 tests) - K8s: HelmUninstallStep_RequiredByDestroy verifies helm-uninstall depends on destroy-prereq - Register DestroyCommand in test DI (CliTestHelper.cs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move confirmation prompts to environment-specific destroy steps Each environment step now owns its own confirmation with full context: - Azure: discovers resources in RG, then asks to confirm deletion - Docker Compose: asks to confirm compose down - Kubernetes: asks to confirm helm uninstall with release name + namespace Pipeline layering: destroy → destroy-{env} (prompt) → action step (no prompt) This means 'aspire do docker-compose-down' skips the prompt (explicit action), while 'aspire destroy' chains through the confirmation layer. The generic destroy-prereq is now a plain no-op placeholder step. --yes skips all confirmation prompts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update deployment E2E tests to use aspire destroy for cleanup Replace manual cleanup commands with 'aspire destroy --yes': - Azure (14 files): add aspire destroy step before exit, keep CleanupResourceGroupAsync as safety net in finally block - Docker (2 tests): replace 'docker compose down' with aspire destroy - Podman (1 test): replace 'podman compose down' with aspire destroy - Kubernetes (1 test): replace 'helm uninstall' with aspire destroy, keep KinD cluster deletion separate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix review findings: non-interactive guard and step wiring - Fail fast when --yes is not set and interactivity is unavailable, instead of silently proceeding with destruction - Consolidate destroy steps: each environment's destroy step does confirm + action in one step, keeping standalone action steps (docker-compose-down, helm-uninstall) clean for aspire do usage - destroy-prereq is now a plain no-op placeholder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clear deployment state after successful destroy The destroy aggregation step now deletes the deployment state file after all environment destroy steps succeed, acting as an implicit cache clear. This ensures the next deploy starts fresh. Removed per-section Azure state cleanup since the whole file is now deleted at the end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: fail-fast non-interactive guard and state cleanup docs - Move non-interactive/--yes check before ARM calls in Azure destroy so it fails fast without doing expensive Azure work - Document that state file deletion is intentional (includes saved parameters — expected for full environment teardown) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve error message when compose file not found during destroy Tell the user to pass the same --output-path they used during deploy, instead of just saying the file doesn't exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Surface stderr in compose down error messages Include the actual error output (e.g. 'Cannot connect to Docker daemon') instead of generic 'ensure runtime is installed' guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Persist deployment state for Docker Compose and Helm Save minimal deployment state during deploy so destroy can verify what was actually deployed: Docker Compose: saves OutputPath, ProjectName, ComposeFilePath to DockerCompose:{name} state section during compose-up Helm: saves ReleaseName, Namespace to Helm:{name} state section during helm-deploy Destroy steps now check for deployment state first and report 'Nothing to destroy' instead of failing with confusing errors when no deployment exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address manual CR feedback 1. Merge Azure destroy step into existing PipelineStepAnnotation 2. Flatten await using blocks to reduce nesting 3. Remove global::Azure prefix (using Azure; works fine) 4. Add IDeploymentStateManager.ClearAllStateAsync for centralized cleanup 5. Per-environment destroy steps now clean up their own state sections 6. Extract shared AspireDestroyAsync helper for E2E test cleanup, removing duplication across 17 test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clear all deployment state on full destroy via ClearAllStateAsync Full destroy is a full reset — clears parameters, Azure config, and per-environment state. Per-section cleanup in environment steps handles partial/scoped operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add deployment summary to destroy output Each environment now adds summary entries showing what was destroyed: - Azure: resource group name + subscription - Docker Compose: environment name - Helm: release name + namespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use ClearAllStateAsync for --clear-cache instead of raw File.Delete Consolidate all state file mutations through IDeploymentStateManager so in-memory state is also reset correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Azure destroy unit tests with mockable state manager Three new tests covering the destroy pipeline for Azure: - WithAzureState: verifies RG discovery and deletion runs - WithNoAzureState: verifies 'Nothing to destroy' message - NonInteractiveWithoutYes: verifies fail-fast with --yes guidance Added InMemoryDeploymentStateManager for stateful test scenarios. Added deploymentStateManager parameter to ConfigureTestServices. Updated all IDeploymentStateManager mocks with ClearAllStateAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use persisted state for destroy operations and add Compose destroy tests Reliability fixes from code review: - Docker destroy now uses saved ComposeFilePath/ProjectName from deployment state instead of recomputing from current model - Helm destroy uses saved ReleaseName/Namespace for both the confirmation prompt and the actual uninstall call - Docker destroy only clears state after successful compose down, preserves state when compose file is missing Test improvements: - Extract InMemoryDeploymentStateManager to shared test code - Add FakeContainerRuntime.WasComposeDownCalled tracking - Add 2 Docker Compose destroy pipeline tests: - WithState: verifies compose down is called via FakeContainerRuntime - WithNoState: verifies 'Nothing to destroy' without calling compose down Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Introduce IHelmRunner abstraction and add Helm destroy tests Extract IHelmRunner interface from HelmDeploymentEngine to enable testability of Helm operations without requiring a real helm binary. - IHelmRunner: abstraction for running helm CLI commands - DefaultHelmRunner: production implementation using ProcessUtil - FakeHelmRunner: test double that tracks calls and returns exit code 0 - Refactor HelmDeployAsync and HelmUninstallAsync to use IHelmRunner - Register DefaultHelmRunner in DI via AddKubernetesInfrastructureCore New tests: - DestroyHelm_WithState: verifies helm uninstall is called with saved release name and namespace from deployment state - DestroyHelm_WithNoState: verifies 'Nothing to destroy' without calling helm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve test quality: observable ARM mocks and deploy→destroy roundtrip - Make TestResourceGroupResource observable with WasDeleteCalled and WasGetResourcesCalled tracking, threaded through ARM client/subscription - Azure destroy test now asserts ARM DeleteAsync and GetResourcesAsync were actually called, not just that the step was created - Add deploy→destroy roundtrip test for Docker Compose that verifies state persisted during deploy is correctly consumed by destroy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename PipelineOptions.Yes to SkipConfirmation Clearer intent — 'Yes' was ambiguous, 'SkipConfirmation' describes exactly what the option does. The CLI flag remains --yes/-y. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add API compat suppressions for new interface members Suppress CP0006 for ClearAllStateAsync added to IDeploymentStateManager and Compose methods added to IContainerRuntime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix unused using in KubernetesEnvironmentExtensions Removes redundant Microsoft.Extensions.DependencyInjection using (only Extensions variant needed for TryAddSingleton). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Azure portal link to destroy summary Link to the resource group in the portal so users can monitor the async deletion operation or diagnose failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extract AzurePortalUrls helper for shared portal URL generation Consolidate portal URL construction into a single shared class: - GetResourceGroupUrl: used by deploy summary and destroy summary - GetDeploymentUrl(string, string, string): used by BicepProvisioner - GetDeploymentUrl(ResourceIdentifier): used by BicepProvisioner Removes duplicate URL construction logic from AzureEnvironmentResource and BicepProvisioner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback - Improve --yes option description to be clearer (JamesNK) - Change confirmation button text from 'Yes, destroy' to 'Destroy' (JamesNK) - Fix Helm: throw on non-zero exit so state isn't cleared on failure (JamesNK) - Fix ClearAllStateAsync: acquire _stateLock before mutating in-memory state to maintain locking discipline (JamesNK) - Fix missing ReportingStep.CompleteAsync when compose file no longer exists during destroy (JamesNK) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use sentence case for destroy status messages Change 'DESTROY COMPLETED'/'DESTROY FAILED' to 'Destroy completed'/ 'Destroy failed' per review feedback (JamesNK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add regression test for Helm uninstall failure preserving state Verifies that when helm uninstall exits non-zero, deployment state is preserved so the user can retry aspire destroy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address remaining review feedback - Lowercase non-proper-noun words in prompt titles (JamesNK) - Revert --force back to --yes pending naming discussion - Fix zero-width character introduced by sed in test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix compose destroy failing on stale build contexts docker compose down validates the compose file before executing, which fails when build contexts referenced in the file no longer exist on disk. This happens in normal deploy→destroy flows when containers were built from temporary contexts. Fix: destroy now uses project-name-only mode (no -f flag) for compose down. The project name from saved deployment state is sufficient — compose looks up running containers by project label. Also make ComposeFilePath optional in ComposeOperationContext so callers can opt into project-name-only mode. Test updated to verify ComposeFilePath is null in destroy context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Wait for Azure resource group deletion to complete Change from WaitUntil.Started to WaitUntil.Completed so destroy actually waits for the RG to be deleted before reporting success. This avoids the confusing 'deletion initiated' message and prevents the stale-state problem where immediate redeploy fails. Also closes #16100 since we now wait for completion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Helm double task completion and improve Azure delete messaging - Remove redundant FailAsync before throw in HelmUninstallAsync to avoid completing the task twice (JamesNK) - Fix 8-space indent in ProvisioningTestHelpers (JamesNK) - Revert Azure delete to WaitUntil.Started (faster for E2E tests) with honest messaging: 'deletion in progress, monitor in Azure portal' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add deletion status to destroy summary Show 'Deletion in progress. Monitor in the Azure portal.' in the pipeline summary so the async nature of RG deletion is visible even after the pipeline output scrolls away. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Show portal URL inline in destroy status summary Make the monitoring link unmissable by showing the full URL rather than hiding it behind 'Azure portal' link text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback from eerhardt - Scope discoveryTask await using block so it disposes before confirmation prompt - Inline Docker compose destroy confirmation message into ConfirmDestroyAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Add
aspire destroy— a new top-level CLI command that tears down previously deployed Aspire environments. Completes the deployment lifecycle:aspire publish → aspire deploy → aspire destroy.What it does
Each compute environment implements its own destroy step with contextual confirmation:
docker compose downusing persisted deployment statehelm uninstallusing persisted release name and namespaceKey design decisions
--yes/-yskips prompts.destroy-{env}step (confirm + action) and a standalone action step (docker-compose-down,helm-uninstall) callable viaaspire dowithout confirmation.--yesin non-interactive mode throws immediately instead of silently proceeding.IDeploymentStateManager.ClearAllStateAsync.New abstractions
WellKnownPipelineSteps.Destroy/DestroyPrereq— pipeline aggregation stepsPipelineOptions.SkipConfirmation— forwarded from CLI--yesflagIDeploymentStateManager.ClearAllStateAsync()— centralized state cleanupIResourceGroupResource.DeleteAsync()/GetResourcesAsync()— ARM abstractions for destroyIHelmRunner— testable abstraction for Helm CLI operationsValidation
--yes), Docker Compose deploy→destroy (interactive +--yes)aspire destroy --yesvia sharedAspireDestroyAsynchelperFixes #13013
Checklist
<remarks />and<code />elements on your triple slash comments?aspire.devissue: